/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.tracking.internal;

import java.beans.*;
import java.util.*;
import org.jdesktop.beansbinding.*;
import ch.trackedbean.tracking.*;

/**
 * Property object for property status binding.<br>
 * This is a read only property!
 * 
 * @param <T> The type of the bean
 * @author M. Hautle
 */
public final class StatusProperty<T> extends PropertyHelper<T, Status> {
    /** The name of the property */
    private final String property;

    /** The base property or null */
    private final Property<T, ? extends StatusInformationInternal> baseProperty;

    /** The map containing all registered map listeners */
    private final IdentityHashMap<T, Listener> map = new IdentityHashMap<T, Listener>();

    /**
     * Creates a Property object listeneing on a property status.
     * 
     * @param <T> The type of the bean
     * @param path The path to the property (see {@link BeanProperty})
     * @return A {@link StatusProperty} object for the given path
     */
    public static <T> StatusProperty<T> create(String path) {
        Property<T, ? extends StatusInformationInternal> p = null;
        final int last = path.lastIndexOf('.');
        if (last > 0) {
            p = BeanProperty.create(path.substring(0, last));
            path = path.substring(last + 1);
        }
        return new StatusProperty<T>(p, path);
    }

    /**
     * Default constructor
     * 
     * @param property The property
     * @param baseProperty A property object returning the bean in which status/propertystatus your interested in
     */
    private StatusProperty(final Property<T, ? extends StatusInformationInternal> baseProperty, final String property) {
        this.property = property;
        this.baseProperty = baseProperty;
    }

    /**
     * @see org.jdesktop.beansbinding.PropertyHelper#getValue(java.lang.Object)
     */
    @Override
    public Status getValue(final T source) {
        final Object src = getBase(source);
        return src instanceof StatusInformationInternal ? ((StatusInformationInternal) src)._getBeanStatus().getStatus(property) : null;
    }

    /**
     * Returns the base object (specified by {@link #baseProperty}) of the given source or source if {@link #baseProperty} is null.
     * 
     * @param source The source object
     * @return The base object for the status property
     */
    private Object getBase(final T source) {
        return baseProperty != null ? baseProperty.getValue(source) : source;
    }

    /**
     * @see org.jdesktop.beansbinding.PropertyHelper#getWriteType(java.lang.Object)
     */
    @Override
    public Class<? extends Status> getWriteType(final T source) {
        return null;
    }

    /**
     * @see org.jdesktop.beansbinding.PropertyHelper#isReadable(java.lang.Object)
     */
    @Override
    public boolean isReadable(final T source) {
        return (baseProperty == null || baseProperty.isReadable(source)) && getBase(source) instanceof StatusInformationInternal;
    }

    /**
     * @see org.jdesktop.beansbinding.PropertyHelper#isWriteable(java.lang.Object)
     */
    @Override
    public boolean isWriteable(final T source) {
        return false;
    }

    /**
     * @see org.jdesktop.beansbinding.PropertyHelper#setValue(java.lang.Object, java.lang.Object)
     */
    @Override
    public void setValue(final T source, final Status value) {
        throw new UnsupportedOperationException();
    }

    /**
     * @see org.jdesktop.beansbinding.PropertyHelper#listeningStarted(java.lang.Object)
     */
    @Override
    protected final void listeningStarted(final T source) {
        if (!map.containsKey(source))
            map.put(source, new Listener(source));
    }

    /**
     * @see org.jdesktop.beansbinding.PropertyHelper#listeningStopped(java.lang.Object)
     */
    @Override
    protected final void listeningStopped(final T source) {
        final Listener entry = map.remove(source);
        if (entry != null)
            entry.cleanUp();
    }

    /**
     * Listener for the {@link IBeanStatus} holding the status informations about the property.
     * 
     * @author M. Hautle
     */
    private class Listener implements PropertyChangeListener, PropertyStateListener {
        /** The source bean */
        private final T source;

        /** The observed bean or null. */
        private IBeanStatus beanState;

        /** The cached value */
        private Object cachedValue;

        /**
         * Default constructor
         * 
         * @param source The source bean
         */
        Listener(final T source) {
            this.source = source;
            if (baseProperty != null)
                baseProperty.addPropertyStateListener(source, this);
            setUpListeners();
            cachedValue = getValue();
        }

        /**
         * Attaches the listeners
         */
        private void setUpListeners() {
            final Object base = getBase(source);
            // don't care about inexistent beans
            if (base == null)
                return;
            if (!(base instanceof StatusInformationInternal))
                throw new IllegalArgumentException("Only classes containing statusinformations are accepted!");
            beanState = ((StatusInformationInternal) base)._getBeanStatus();
            beanState.addStatusChangeListener(property, this);
        }

        /**
         * Removes the map listener
         */
        private void removeListeners() {
            if (beanState != null)
                beanState.removeStatusChangeListener(property, this);
            beanState = null;
        }

        /**
         * Cleans up this listener
         */
        void cleanUp() {
            removeListeners();
            if (baseProperty != null)
                baseProperty.removePropertyStateListener(source, this);
            cachedValue = null;
        }

        /**
         * Updates the property value if necessary.
         * 
         * @param newValue The new value
         */
        private void updateProperty(final Object newValue) {
            final Object oldValue = cachedValue;
            if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
                cachedValue = newValue;
                firePropertyStateChange(new PropertyStateEvent(StatusProperty.this, source, true, oldValue, newValue, false, false));
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            updateProperty(evt.getNewValue());
        }

        /**
         * Returns the current state value of the property.
         * 
         * @return The value (may be null if there is no owning bean)
         */
        private Object getValue() {
            if (beanState == null)
                return null;
            return beanState.getStatus(property);
        }

        /**
         * {@inheritDoc}
         */
        public void propertyStateChanged(final PropertyStateEvent pse) {
            if (!pse.getValueChanged())
                return;
            // adapt the listeners
            removeListeners();
            setUpListeners();
            updateProperty(getValue());
        }
    }
}
